package bsh.compilation;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestListener;
import junit.framework.TestResult;
import bsh.CompiledScript;
import bsh.CompilerImpl;
import bsh.ParseException;
import bsh.Prototype;
import bsh.exception.CompilationFailedException;
import bsh.utils.reflection.OpenClassLoader;

public abstract class AbstractCompilationTestCase extends TestCase {

    protected Binding fields = new Binding(), dynamicFields = new Binding();

    protected static long seed = 1;

    public static CompilerImpl c = new CompilerImpl();

    public TypeWithFields someType;

    public static final OpenClassLoader loader = new OpenClassLoader();

    protected byte[] lastBytecode;

    protected Map<String, byte[]> allByteCodes;

    protected void writeBytecode() {
        if (lastBytecode != null) {
            try {
                FileOutputStream fout = new FileOutputStream(new File("target", this.getName() + ".class"));
                fout.write(lastBytecode);
                fout.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (allByteCodes != null) {
            for (Entry<String, byte[]> entry : allByteCodes.entrySet()) {
                try {
                    FileOutputStream fout = new FileOutputStream(new File("target", entry.getKey() + ".class"));
                    fout.write(entry.getValue());
                    fout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }
    }

    private class WriteBytecodeListener implements TestListener {

        public void addError(Test test, Throwable t) {
            writeBytecode();
        }

        public void addFailure(Test test, AssertionFailedError t) {
            writeBytecode();
        }

        public void endTest(Test test) {
        }

        public void startTest(Test test) {
        }

    }

    @Override
    public void run(TestResult result) {
        result.addListener(new WriteBytecodeListener());
        super.run(result);
    }

    @Override
    protected void setUp() throws Exception {
        this.fields.vars.clear();
        this.someType = new TypeWithFields();
        this.put("value", this.someType);
    }

    public Object execute(final String text) {
        if (!text.endsWith(";")) {
            return this.execute(text + ";");
        }
        try {
            return execute(compile(text));
        } catch (Exception e) {
            if (e.getCause() != null) {
                throw new IllegalStateException(e.getCause());
            } else {
                throw new IllegalStateException(e);
            }
        }
    }

    public Object execute(Class<? extends CompiledScript> defineClass) throws InstantiationException, IllegalAccessException, Exception {
        CompiledScript prototype = newScriptInstance(defineClass);
        return doExecuteScript(prototype);
    }

    public Object doExecuteScript(CompiledScript prototype) throws Exception {
        final Object r = prototype.executeScript();
        List<String> list = new ArrayList<String>(this.fields.vars.keySet());
        for (String string : list) {
            this.put(string, prototype.getFieldValue(string));
        }
        return r;
    }

    public CompiledScript newScriptInstance(Class<? extends CompiledScript> defineClass) throws InstantiationException,
            IllegalAccessException {
        CompiledScript prototype = defineClass.newInstance();
        // results.getBytecode()
        prototype.setFields(this.fields.vars);
        for (Entry<String, Object> entry : this.dynamicFields.vars.entrySet()) {
            prototype.setDynamicField(entry.getKey(), entry.getValue());
        }
        return prototype;
    }

    @SuppressWarnings("unchecked")
    public Class<? extends CompiledScript> compile(final String text) throws ParseException, CompilationFailedException {
        final HashMap<String, Class<?>> hashMap = new HashMap<String, Class<?>>();

        Set<Entry<String, Object>> set = this.fields.vars.entrySet();
        for (Entry<String, Object> entry : set) {
            Class<? extends Object> value = (entry.getValue() != null) ? entry.getValue().getClass() : Object.class;
            hashMap.put(entry.getKey(), value);
        }
        CompilationResults results =
                c.compileOverrideOptions(
                        new AbstractCompilationSource(this.newName(), hashMap, new HashMap<String, Object>(), this.getName(),
                                Prototype.class) {

                            public Reader openReader() {
                                return new StringReader(text);
                            }
                        }, new DefaultCompilationOptions() {
                            @Override
                            public boolean allowUndefinedVariables() {
                                return true;
                            }

                            @Override
                            public Set<DebugOptions> debugOptions() {
                                return AbstractCompilationTestCase.this.debugOptions();
                            }

                        });
        this.lastBytecode = results.getBytecode();
        this.allByteCodes = results.getDependentTypes();
        return (Class<? extends CompiledScript>) loader.load(results);
    }

    public Set<DebugOptions> debugOptions() {
        return EnumSet.allOf(DebugOptions.class);
    }

    public String newName() {
        return "generated/ScriptBase" + seed++;
    }

    public static class Binding {
        public final Map<String, Object> vars = new HashMap<String, Object>();

        public Binding put(String name, Object val) {
            this.vars.put(name, val);
            return this;
        }

    }

    public Binding put(String name, Object val) {
        return this.fields.put(name, val);
    }

    public Object executeClasspathResource(final String resource) {
        try {
            CompilationSource compilationSource = toCompilationSource(resource);
            return compileExecute(compilationSource);
        } catch (Exception e) {
            if (e.getCause() != null) {
                throw new IllegalStateException(e.getCause());
            } else {
                throw new IllegalStateException(e);
            }
        }
    }

    public CompilationSource toCompilationSource(final String resource) {
        final HashMap<String, Class<?>> hashMap = new HashMap<String, Class<?>>();
        Set<Entry<String, Object>> set = this.fields.vars.entrySet();
        for (Entry<String, Object> entry : set) {
            Class<? extends Object> value = (entry.getValue() != null) ? entry.getValue().getClass() : Object.class;
            hashMap.put(entry.getKey(), value);
        }
        String source;
        if (resource.startsWith("/")) {
            source = resource.substring(1);
        } else {
            source = resource;
        }
        CompilationSource compilationSource =
                new AbstractCompilationSource(this.newName(), hashMap, new HashMap<String, Object>(), source) {

                    public Reader openReader() {
                        return new InputStreamReader(AbstractCompilationTestCase.this.getClass().getResourceAsStream(resource));
                    }
                };
        return compilationSource;
    }

    public Object compileExecute(CompilationSource compilationSource) throws ParseException, CompilationFailedException,
            InstantiationException, IllegalAccessException, Exception {
        Class<? extends CompiledScript> defineClass = compileDefineClass(compilationSource);
        return execute(defineClass);
    }

    @SuppressWarnings("unchecked")
    public Class<? extends CompiledScript> compileDefineClass(CompilationSource compilationSource) throws ParseException,
            CompilationFailedException {
        CompilationResults results = c.compileOverrideOptions(compilationSource, new DefaultCompilationOptions() {
            // @Override
            // public Set<DebugOptions> debugOptions() {
            // return EnumSet.noneOf(DebugOptions.class);
            // }

            @Override
            public boolean allowUndefinedVariables() {
                return true;
            }

            @Override
            public boolean initializeLocalVariables() {
                return false;
            }

            @Override
            public boolean inlineConstants() {
                return true;
            }

            @Override
            public Set<DebugOptions> debugOptions() {
                return AbstractCompilationTestCase.this.debugOptions();
            }

        });
        this.lastBytecode = results.getBytecode();
        allByteCodes = results.getDependentTypes();
        Class<? extends CompiledScript> defineClass = (Class<? extends CompiledScript>) loader.load(results);
        return defineClass;
    }

    public Object get(String name) {
        return this.fields.vars.get(name);
    }

    @SuppressWarnings("unchecked")
    public Class<? extends CompiledScript> loadClass(CompilationResults results) {
        return (Class<? extends CompiledScript>) loader.defineClass(results.className(), results.getBytecode());
    }

    public void writeToFile(CompilationResults res) {
        writeToFile(res.className(), res.getBytecode());
    }

    public void writeToFile(String className, byte[] data) {
        File file = new File("target", className.replace('/', '.') + ".class");
        try {
            FileOutputStream fout = new FileOutputStream(file);
            fout.write(data);
            fout.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}
